# -*- coding: utf-8 -*-
"""
This script has realized after joining two other frameworks:
	* basically: Python IRC library (AKA python-irclib AKA irclib.py)
		http://python-irclib.sourceforge.net/ (v. 0.4.8)
	* ircbot.py (AKA PhennyBot)
		http://inamidst.com/proj/ircbot/ircbot.py
Other inspiring scripts:
	* one instance of twisted ircbot (feature_list).
	* shield_in() is based on sentinel function from eggdrop
		http://www.egghelp.org/
"""
import os, random, re, socket, sys, thread, time, types
from getpass import getpass
from time import sleep as wait
from select import select as fetch

from util.functions import *
from ircfuncs import _trans, ircformat, is_channel, skip_shield, Features, parse_channel_modes, escape_for_regexp
from ircvars import _ctcp_re, _ircmsg_re, _linesep_re, _maxlen, _source_re, numeric_events, all_events
from optimizers import _path, Channel, Command, Dicts, Event, File, IRCDict, Load, Logs, printf, fmt, _path
from speech import replies

#next 3 vars are for debugging.
_debug = (
	False, #input
	True, # output
	False, # buffer
	False, # auto
	False # recentchanges
)
_skip_cmd = ("motd", "featurelist")
_show_cmd = ("notice", "privmsg", "whoisaccount")

class IRCError:
	pass

class BadBotDescriptor:
	pass

class Streaming:
	def __init__(self, bot, addr):
		self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
		s.bind(addr)
		print " UDP listener CONNECTED ".center(34, "*")
		while True:
			(r, w, x) = fetch([s], [], [], .2)
			if r:
				s = r[0]
				data, addr = s.recvfrom(1024)
				bot.ctrl_msg(evt,
					Event(data), #TODO: need Event object data
					decode(data)
				)

class Bot:
	def __init__(self, cfg, client):
		#self.label = cfg.label
		self.nickname = cfg.user['nickname']
		self.ircname = cfg.user['ircname']
		self.realname = cfg.user['realname']
		self.umode = cfg.user['mode']
		passw = cfg.user['passw'] if cfg.user['passw'] and \
				isinstance(cfg.user['passw'], basestring) else \
				getpass("Enter your user password:") if cfg.user['passw'] else None
		self.user_passw = passw

		self.defchan = cfg.server['chan']
		self.testchan = cfg.server['testchan'] \
			if cfg.server.has_key('testchan') else cfg.server['chan']
		self.host = cfg.server['host']
		self.port = cfg.server['port']
		self.server_passw = cfg.server['passw']

		self.lang = cfg.extra['lang']
		self.folders = cfg.extra['folders']

		self.version_id = client['version']
		self.help_link = client['help link']
		self.on_server = client['server link']
		self.svn = client['svn']

		self._socket = None
		self._previous_buffer = ""
		self.current_sname = ""
		self.current_nname = self.nickname
		self._features = {
			'CHANNELLEN': 200,
			'CHANTYPES': tuple('#&'),
			'MODES': 3,
			'NICKLEN': 9,
			'PREFIX': '(ov)@+'
		}
		self.bots = []
		if cfg.extra.has_key("killword"): self.killword = cfg.extra['killword']
		if hasattr(cfg, "udp_listener") and isinstance(cfg.udp_listener, list):
			self.udp_listener = cfg.udp_listener
		self._debug = _debug
		self._skip_shieldout = False
		self.events = {}

	def _connect(self):
		if self._debug[1]:
			shellmsg = "$me has connected to $server"
			printf(shellmsg, me = self.nickname, server = self.host)
		self._socket = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
		self._socket.settimeout(60*60*24)
		self._socket.connect ( (self.host, self.port) )
		self.nick(self.nickname)
		self.user(self.ircname, realname="geni 2.0")
		self.mode(self.nickname, self.umode)
		self._listen()

	def _streaming(self):
		if self.label[1] == 0 and hasattr(self, "udp_listener"):
				thread.start_new_thread(Streaming, (self, self.udp_listener))

	def _listen(self):
		while True:
			(r, w, x) = fetch([self._socket], [], [], .2)
			if r:
				socket = r[0]
				buffer = socket.recv ( 4096 )
				buffer = decode(buffer)
				self._incoming_data(buffer)

	def _incoming_data(self, new_data):
		lines = _linesep_re.split(self._previous_buffer + new_data)
		self._previous_buffer = lines.pop()

		for buffer in lines:
			prefix = None; command = None; parameters = None
			parsed_line = _ircmsg_re.match(buffer).groupdict()
			prefix = parsed_line['prefix']
			command = parsed_line['command']
			parameters = parsed_line['parameters']

			if not buffer:
				continue

			if not self.current_sname:
				self.current_sname = prefix

			command = command.lower()
			command = numeric_events.get(command, command)

			if self._debug[0]  and command not in _skip_cmd:
				print "FROM SERVER:", time.time(), buffer

			if ":" in parameters:
				parameters, msg = parameters.split(" :",1)
				parameters = parameters.strip().split()
			else:
				parameters, msg = [parameters.strip()], ""
			if command == "mode": parameters = parameters[0].split(" ")

			stalked = bool(re.search("(?<!#)%s" % escape_for_regexp(self.current_nname), msg))
			src = _source_re.search(prefix).groupdict() if prefix else None
			target = None; source = None; agent="server";
			if src:
				source = src['nname']
				target = parameters[0] if parameters and parameters[0] != self.current_nname else source
				if isinstance(src['hname'], basestring):
					if src['hname']=="services.":
						agent = "services"
					elif src['hname'].count("/") == 2 and src['hname'].split("/")[0] == "freenode":
						agent = src['hname'].split("/")[1] #for staffs, utility-bots
					else:
						agent="user"
			ctcp=None
			if command in ("notice", "privmsg"):
				command, msg, ctcp = self._check_msg(command, target, msg)

			if self._debug[0]: #not in _skip_cmd:
				print "\tSOURCE: %r\n\tTARGET: %r\n\tCMD: %r\n\tPRM: %r\n\tMSG: %r\n\tAGT: %r\n\tCTCP: %r\n\tSTK: %r" % (source, target, command, parameters, msg, agent, ctcp, stalked)
			event = Event(src, source, target, command, parameters, msg, agent, ctcp, stalked)
			#self.event = event #event can not be modified here or bot fails the targets and all others methods.
			event.id = ''.join(random.choice('0123456789abcdefABCDEF') for i in range(8))

			#handle built-in events
			if command  in (
				"ping", "nick", "welcome", "nicknameinuse",
				"featurelist", "whoisaccount", "whoisuser",
				"join", "kick", "mode", "namreply", "part",
				"quit",	"error"
			):
				f = getattr(self, "_on_%s" % command)
				f(event)
			self._handle_event(event)
			triggers = self.channels_extended[event.target]['triggers'] if is_channel(event.target) \
				and self.channels_extended.has_key(event.target) else self.triggers
			usrinput = Command(msg, triggers, self.current_nname)
			self._handle_command(event, usrinput)

	def _check_msg(self, command, target, msg):
		field = "pub" if is_channel(target) else "priv"
		if command == "privmsg": command = "msg"
		ctcp = _ctcp_re.search(msg.lower())
		if ctcp:
			tag = ctcp.group("tag")
			params = ctcp.group("parameters")
			if tag == "action":
				command = tag
				msg = params
			elif tag == "dcc":
				field=""
				p = params.split(" ", 1)
				command = "%s_%s" % (tag, p[0].lower())
				msg = p[1]
			else:
				field=""
				command="ctcp"
				msg=params
		command = "%s%s" % (field, command)
		return command, msg, ctcp.groups() if ctcp else None

	def _handle_event(self, evt):
		for e in all_events:
			if evt.type == e and hasattr(self, "on_%s" % e):
				f = getattr(self, "on_%s" % e)
				try:
					if f(evt): break
				except:
					self.send_error(evt)

	def _handle_command(self, evt, cmd):
		if re.match("priv|pub", evt.type) and not evt.msg:
			return
		if re.match("priv|pub", evt.type) and evt.msg.startswith("#"):
			return
		#if re.search("^[%s \)\(\*\=]+$" % self.triggers, evt.msg): return

		def call(fn):
			try:
				return fn(self, evt, cmd)
			except:
				self.send_error(evt)
				return True

		def do_fn(fn):
			if fn.thread:
				rtn = thread.start_new_thread(call, (fn,))
			else:
				rtn = call(fn)
			#if fn returns True, loop for commands will be break
			return rtn

		def switch(regexp):
			pattern=regexp.pattern
			if "$me" in pattern:
				pattern = pattern.replace("$me", self.current_nname)
			elif "$trigger" in pattern:
				pattern = pattern.replace("$trigger", "[%s]" % triggers)
			elif "$both" in pattern:
				pattern = pattern.replace("$both", u"[%s]|%s: " %(triggers, self.current_nname))
			return re.compile(pattern, re.I)

		def watch():
			if len(cmd.opts)==1 and cmd.opts.keys()[0] in ("help", "?", "url"):
				if cmd.opts.has_key("help") or cmd.opts.has_key("?"):
					self.reply(evt, fn.help)
					return False
				elif cmd.opts.has_key("url"):
					if hasattr(fn, "url"):
						if isinstance(cmd.opts['url'], basestring) and "%s" in fn.url:
							if fn.url.count("%")>1 and hasattr(fn, "def_url"): fn.url = fn.def_url
							else: fn.url = fn.url.replace("%s","", fn.url.count("%")-1)
							self.reply(evt, fn.url % cmd.opts['url'])
						elif not isinstance(cmd.opts['url'], basestring) and "%s" in fn.url:
							self.reply(evt, fn.url.replace("%s","<param>",1))
						else: self.reply(evt, fn.url)
						return False
					else:
						self.reply(evt, "%s has not url.", cmd.command)
						return False
			return do_fn(fn)
		evt.level = self.accounts.get(self.nicks.get(evt.source), {"level":"user"}).get("level")
		evt.chan = evt.target if is_channel(evt.target) else ""
		if evt.type == "join" and not evt.chan:
			if self.features.network()=="freenode":
				evt.chan = evt.target = evt.params[0].strip()
			elif self.features.network()=="Wikimedia":
				evt.chan = evt.target = evt.msg

		triggers = self.channels[evt.chan].triggers if evt.chan and self.channels.has_key(evt.chan) else self.triggers
		if evt.chan and self.channels.has_key(evt.chan) and not self.channels[evt.chan].listen: return
		if is_channel(evt.target) and self.channels.has_key(evt.target):
			evt.lang = self.channels[evt.target].lang
			evt.fam = self.channels[evt.target].fam
		elif evt.agent == "user" and self.accounts.has_key(self.nicks.get(evt.source)):
			evt.lang = self.accounts[self.nicks[evt.source]]['lang']
			evt.fam = self.accounts[self.nicks[evt.source]]['fam']
		else:
			evt.lang = "en"
			evt.fam = "w"

		try:
			done = False; break_loops = None
			for priority in ("skip", "high", "medium", "low"):
				for func in self.commands[priority]:
					fn = self.commands[priority][func]
					if hasattr(fn, "wherelang") and evt.lang != fn.wherelang: continue
					if fn.evttypes[0] == "*":
						do_fn(fn)
						continue
					elif re.search("|".join(fn.evttypes), evt.type):
						if not re.match("priv|pub", evt.type):
							search_str = evt.type
						else:
							search_str = evt.msg if fn.fullmsg else cmd.command
					else:
						continue
					if not search_str: continue
					aliases = fn.aliases+fn.rule
					if evt.chan:
						if fn.onchan and evt.chan not in fn.onchan:
							continue
						if self.channels.has_key(evt.chan) and fn.chanlang.get(self.channels[evt.chan].lang):
							aliases+= fn.chanlang.get(self.channels[evt.chan].lang)
						elif fn.channels.has_key(evt.chan):
							aliases = [fn.channels[evt.chan]]
					for alias in aliases:
						if not isinstance(alias, basestring):
							alias = switch(alias)
							regexp = alias.search(search_str)
							if regexp:
								cmd.dict = regexp.groupdict()
								cmd.groups = regexp.groups()
								if evt.level not in skip_shield:
									check, alert = self._shield_in(fn, evt)
									if not check:
										return
								break_loops = watch()
								done = True
						else:
							if alias == search_str:
								if evt.level not in skip_shield:
									check, alert = self._shield_in(fn, evt)
									if not check:
										return
								break_loops = watch()
								done = True
						if done: break
						if break_loops: break
					if break_loops: break
				if break_loops: break
			if break_loops and _debug[3]: print "LOOP BROKEN AFTER %s()" % fn.name
		except RuntimeError:
			#if we add a new command, command dictionary grows and then it raises a RuntimeError.
			print "There are new functions..."

		if not evt.agent.startswith("serv") and not self.ignored.has_key(evt.source)\
		and not done and cmd.command:
			if evt.level not in skip_shield :
				if not self._requests.has_key(evt.source):
					self._requests[evt.source]={"unknown":(time.time(),1)}
				else:
					times=self._requests[evt.source]["unknown"][1]+1 if self._requests[evt.source].has_key("unknown") else 1
					self._requests[evt.source]["unknown"]=(time.time(), times)
					if times>3:
						return
			self.choose_reply(evt, replies.unavcmd)

	def _shield_in(self, fn, evt):
		"""Prevent flooders that try bot fails for excess flood."""
		#if fn.ignore:return True
		if evt.src.get('hname') == "special.user" and evt.source == "rc-pmtpa" and self.features.network() == "Wikimedia": return True, "T#0"
		if evt.source == self.current_nname or not re.search ("(priv|pub)(msg|notice|action)", evt.type): return True, "T#1"
		if evt.agent.startswith("serv"): return True, "T#2"
		if evt.source in self.logs.otherbots.get(): return False, "T#2.1"

		now = time.time()
		#penalties: low 1 minute, high 12 minutes
		min_pain = 60; max_pain = 60 * 12
		#maximum default time for maintaining a penalty
		max_time = 60 * 3 if not self.ignored.has_key(evt.source) else self.ignored[evt.source][1]

		#clean old data
		for user in self._requests.copy():
			for request in self._requests[user].copy():
				if now - self._requests[user][request][0] > max_time:
					del self._requests[user][request]
					if self.ignored.has_key(user): self.ignored.pop(user)

		#user is ignored, update this new request and exit!
		if self.ignored.has_key(evt.source):
			if self._requests.has_key(evt.source) and self._requests[evt.source].has_key(fn.name):
				self._requests[evt.source][fn.name] = (now, self._requests[evt.source][fn.name][1] + 1)
			#print "\tignoring %s since %s" % (evt.source, time.strftime("%y-%m-%d %H:%M:%S", time.localtime(self.ignored[evt.source])))
			return False, "F#1"

		#new user
		if not self._requests.has_key(evt.source):
			self._requests[evt.source] = {fn.name: (now, 1)}
			return True, "T#3"
		#new command for known user
		elif not self._requests[evt.source].has_key(fn.name):
			self._requests[evt.source][fn.name] = (now, 1)
			return True, "T#4"

		last_time, times = self._requests[evt.source][fn.name]

		#more than one request in less than 1 sec., penalty: 12 min ignored
		#this method may suggest the ban of the user.
		if now - last_time < 1:
			self._requests[evt.source][fn.name] = (now, times + 1)
			self.ignored.update({evt.source:(now, max_pain)})
			#print "\t%s is ignored, s/he abused with %s command in %.3f sec. for %i times" % (evt.source, fn.name, now-last_time, times)
			return False, "F#2"

		self._requests[evt.source][fn.name] = (now, times + 1)

		#too requests! requests > func.limit, penalty: 60 sec ignored
		if times >= fn.limit and now - last_time < max_time:
			self.ignored.update({evt.source:(now, min_pain)})
			#print "\ttoo _requests by %s with %s command at %s for %i times" % (evt.source, fn.name, time.strftime("%y-%m-%d %H:%M:%S", time.localtime(now)), times)
			return False, "F#3"

		#more than five diferent requests in less than 2 min, penalty 60 sec
		if now - last_time < 120 and len(self._requests[evt.source]) > 5:
			self.ignored.update({evt.source:(now, min_pain)})
			#print "\ttoo diferent _requests by %s with %s command at %s in 3 min" % (evt.source, fn.name, time.strftime("%y-%m-%d %H:%M:%S", time.localtime(now)))
			return False, "F#4"
		return True, "T#5"

	def _shield_out(self, cmd, args, msg):
		"""[Internal]
		* prevent flood due to sending long messages
		  * control string length
		  * control consecutive message rate
		"""
		if cmd.lower() not in ("privmsg","notice"): return True
		if self._skip_shieldout: return True

		length = len(msg)
		self._verbosity['acculen'] += length
		if time.time() - self._verbosity['lastmsg'] > 10:
			self._verbosity['acculen'] = length
			#return False
		self._verbosity['lastmsg'] = time.time()
		if self._verbosity['acculen'] > 800:
			time.sleep(self._verbosity['acculen'] / 100)
		elif self._verbosity['acculen'] > 400:
			time.sleep(4)
		elif self._verbosity['acculen'] > 300:
			time.sleep(1)
		elif self._verbosity['acculen'] > 100:
			pass
		elif self._verbosity['acculen'] <= 100 and time.time() - self._verbosity['lastmsg'] > 1:
			self._verbosity['acculen']=0
		return True

	def _replace_templates(self, evt, string):
		if not evt: return string
		projcond = self.channels[evt.target].fam \
			if is_channel(evt.target) and \
			self.channels.has_key(evt.target) \
			else self.accounts.get(self.nicks.get(evt.target),
								   "wikipedia")

		if evt.stalked:
			if not evt.type.startswith("priv"):
				if not evt.agent.startswith("serv"):
					if "$nick" not in string:
						action = re.search(ur"^\x01ACTION (.+)\x01$", string)
						if action:
							string = u"\x01ACTION $nick: %s\x01" % action.group(1)
						else:
							string = "$nick: %s" % string
		return fmt(string,
				   me = self.current_nname,
				   nick = evt.source,
				   owner = self.owner,
				   chan = evt.target,
				   serv = self.current_sname,
				   proj = projcond,
				   more = self.help_link
		)

	def _replace_mnemos(self, obj):
		if not hasattr(self, "mnemo"): return obj

		if isinstance(obj, basestring):
			if not "_" in obj: return obj
			for word in obj.split(" "):
				if word.startswith("_") and self.mnemo.has_key(word[1:]):
					obj=re.sub(r"\b%s\b" % word, str(self.mnemo[word[1:]]), obj)
		else:
			tempobj = list(obj); i = 0
			for string in tempobj:
				if not "_" in string: continue
				for word in string.split(" "):
					if word.startswith("_") and self.mnemo.has_key(word[1:]):
						string=re.sub(r"\b%s\b" % word, self.mnemo[word[1:]], string)
				obj[i] = string
				i+=1
		return obj

	def _check_send(self, command, arguments=[], message=""):
		"""
		* limit the string length
		* avoid flooding
		"""
		#Can bot reply on channel?
		#if re.match("^(?:PRIVMSG|NOTICE)$", command) and \
		#	is_channel(arguments) and self.channels.get(arguments) \
		#	and not self.channels[arguments].reply:
		#		printf ("SKIP $1 can't reply on $2 [$3|$4]",
		#				command,
		#				evt.target,
		#				arguments,
		#				message,
		#		)
		#		return
		#
		self._queue = None
		if isinstance(arguments, basestring): arguments = [arguments]
		arguments = " ".join(arguments)
		arguments = self._replace_mnemos(arguments)
		if message:
			message = ircformat(message)
			message = self._replace_mnemos(message)
			message = " :%s" % message
		msgpos = len(u"%s %s :" % (command, arguments))
		msglen = _maxlen - msgpos

		if message and len(message) > msglen:
			curmsg = " ".join(message[:msglen+msgpos].split(" ")[:-1])
			nxtmsg = message.replace(curmsg,"")

			self._queue = (command, arguments, nxtmsg)
			message = curmsg + ircformat(" &B;&l;%s&N;" % "↙")
		command = encode(command)
		arguments= encode(arguments)
		message = encode(message)
		string = "%s %s%s" % (command, arguments, message)
		if self._shield_out(command, arguments, message):
			self._skip_shieldout = False
			self.send(string)

	def send(self, string):
		"""send to server."""
		if self._socket is None:
			raise ServerNotConnectedError, "Not connected."
		try:
				self._socket.send(string + "\r\n")
				if self._debug[1]:
					print "TO SERVER:", time.time(), string
		except socket.error, x:
			# Ouch!
			self.disconnect("Connection reset by peer.")
		if self._queue:
			command, arguments, message = self._queue
			self._check_send(command, arguments, message)


	#RFC 1459 output commands
	def action(self, target, action):
		"""Send a CTCP ACTION command."""
		self.ctcp(target, "ACTION", action)

	def admin(self, server=""):
		"""Send an ADMIN command."""
		self._check_send("ADMIN", server)

	def away(self, msg):
		"""Send an AWAY command."""
		pass

	def ctcp(self, target, ctcptype, parameter=""):
		"""Send a CTCP command."""
		ctcptype = ctcptype.upper()
		if parameter: parameter=" %s" % parameter
		self.privmsg(target, "\x01%s%s\x01" % (ctcptype, parameter))

	def ctcp_reply(self, target, parameter):
		"""Send a CTCP REPLY command."""
		self.notice(target, "\x01%s\x01" % parameter)

	def die(self):
		"""Send a DIE command."""
		pass

	def disconnect(self, message=""):
		"""Hang up the connection.

		Arguments:

			message -- Quit message.
		"""
		# Note that many IRC servers don't use your DISCONNECT message
		# unless you've been connected for at least 5 minutes!
		if not self.connected:
			return

		self.connected = 0

		self.quit(message)

		try:
			self._socket.close()
		except socket.error, x:
			pass
		self._socket = None

	def globops(self, text):
		"""Send a GLOBOPS command."""
		self._check_send("GLOBOPS", message = text)

	def info(self, server=""):
		"""Send an INFO command."""
		self._check_send("INFO", server)

	def invite(self, nick, channel):
		"""Send an INVITE command."""
		self._check_send("INVITE",(nick, channel))

	def ison(self, nicks):
		"""Send an ISON command.

		Arguments:

			nicks -- List of nicks.
		"""
		self._check_send("ISON", nicks)

	def join(self, channel, key=""):
		"""Send a JOIN command."""
		self._check_send("JOIN", (channel, key))

	def kick(self, channel, nick, comment=""):
		"""Send a KICK command."""
		self._check_send("KICK", (channel, nick), comment)

	def kill(self, client, comment):
		"""Send a KILL command."""
		pass

	def links(self, remote_server="", server_mask=""):
		"""Send a LINKS command."""
		command = "LINKS"
		if remote_server:
			command += " %s" % remote_server
		if server_mask:
			command += " %s" % server_mask
		self._check_send(command)

	def list(self, channels="", server=""):
		"""Send a LIST command."""
		command = "LIST"
		if channels:
			if isinstance(channels, list):
				channels = ",".join(channels)
			command += " " + channels
		if server:
			command += " " + server
		self._check_send(command)

	def lusers(self, server=""):
		"""Send a LUSERS command."""
		self._check_send("LUSERS", server)

	def mode(self, target, command):
		"""Send a MODE command."""
		self._check_send("MODE", (target, command))

	def motd(self, server=""):
		"""Send an MOTD command."""
		self._check_send("MOTD", server)

	def names(self, channels=""):
		"""Send a NAMES command."""
		if isinstance(channels, list):
			channels = ",".join(channels)
		self._check_send("NAMES", channels)

	def nick(self, newnick):
		"""Send a NICK command."""
		self._check_send("NICK", newnick)

	def notice(self, target, text):
		"""Send a NOTICE command."""
		self._check_send("NOTICE", target, text)

	def oper(self, nick, password):
		"""Send an OPER command."""
		self._check_send("OPER", (nick, password))

	def part(self, channels, message=""):
		"""Send a PART command."""
		# Note that many IRC servers don't use your PART message
		# unless you've been connected for at least 5 minutes!
		if isinstance(channels, list):
			channels = ",".join(channels)
		self._check_send("PART", channels, message)

	def pass_(self, password):
		"""Send a PASS command."""
		self._check_send("PASS", password)

	def ping(self, target, target2=""):
		"""Send a PING command."""
		if target2: target += " %s" % target2
		self._check_send("PING", target)

	def pong(self, target, target2=""):
		"""Send a PONG command."""
		if target2: target += " %s" % target2
		self._check_send("PONG", target)

	def privmsg(self, target, text):
		"""Send a PRIVMSG command."""
		self._check_send("PRIVMSG", target, text)

	def privmsg_many(self, targets, text):
		"""Send a PRIVMSG command to multiple targets."""
		#TODO: no more than 4 in freenode
		if isinstance(targets, list): targets = ",".join(targets)
		self._check_send("PRIVMSG", targets, text)

	def quit(self, message=""):
		"""Send a QUIT command."""
		# Note that many IRC servers don't use your QUIT message
		# unless you've been connected for at least 5 minutes!
		self._check_send("QUIT", message=message)

	def rehash(self):
		"""Send a REHASH command."""
		pass

	def restart(self):
		"""Send a RESTART command."""
		pass

	def service(self, (nickname, reserved, distribution, rserved), info):
		"""Send a SERVICE command."""
		pass

	def servlist(self, mask, type):
		"""Send a SERVLIST command."""
		pass

	def squery(self, servicename, msg):
		"""Send a SQUERY command."""
		pass

	def squit(self, server, comment=""):
		"""Send an SQUIT command."""
		self._check_send("SQUIT", server, comment)

	def stats(self, statstype, server=""):
		"""Send a STATS command."""
		if server: server=" " % server
		self._check_send("STATS", (statstype, server))

	def summon(self, user, server, chan):
		"""Send a SUMMON command."""
		pass

	def time(self, server=""):
		"""Send a TIME command."""
		self._check_send("TIME", server)

	def topic(self, channel, new_topic=""):
		"""Send a TOPIC command."""
		self._check_send("TOPIC", channel, new_topic)

	def trace(self, target=""):
		"""Send a TRACE command."""
		self._check_send("TRACE", target)

	def user(self, username, mode="+iws", unused="*", realname="nemesian"):
		"""Send a USER command.
		RFC 1459: USER <username>	<hostname>	<servername>	<realname>
		RFC 2812: USER <user> 		<mode> 		<unused> 		<realname>
		"""
		self._check_send("USER", (username, mode, unused), realname)

	def userhost(self, nicks):
		"""Send a USERHOST command."""
		nicks = ",".join(nicks)
		self._check_send("USERHOST", nicks)

	def users(self, server=""):
		"""Send a USERS command."""
		self._check_send("USERS", server)

	def version(self, server=""):
		"""Send a VERSION command."""
		self._check_send("VERSION", server)

	def wallops(self, text):
		"""Send a WALLOPS command."""
		self._check_send("WALLOPS", message=text)

	def who(self, target="", op=""):
		"""Send a WHO command."""
		self._check_send("WHO", (target, op))

	def whois(self, target):
		"""Send a WHOIS command."""
		if not hasattr(self, "_whois"):
			self._whois = {}
		#clean buffer
		if len(self._whois) > 10 and time.time() - sorted(self._whois.values())[0] > 11:
			dels=[]
			for k in self._whois.keys():
				if time.time()-self._whois[k]>11:
					dels.append(k)
			for k in dels:
				self._whois.pop(k,"")
		if self._whois.has_key(target):
			t = time.time()-self._whois.get(target)
			if t < 10:
				return
		self._whois[target]=time.time()
		self._check_send("WHOIS", target)

	def whowas(self, nick, max="", server=""):
		"""Send a WHOWAS command."""
		self._check_send("WHOWAS", (nick, max, server))

class SimpleIRCBot(Bot):
	def __init__(self, cfg, client):
		Bot.__init__(self, cfg, client)
		#shield out (self-flood filter): prevent excess flood
		self._verbosity = {"lastmsg": time.time(), "acculen": 0}

		#shield in (incoming protection): prevent attacks and abuses
		self._requests = {} #{nick: {last_cmd: (last_time, times)}}
		self.ignored = {} #{nick: (last_time, ignored_time_penalty)}

		self.nicks = {}
		self.channels = IRCDict()

		root = _path.join(client['basedir'], "logs")
		self.accounts = Dicts("accounts", root)
		for user in self.accounts.keys():
			if self.accounts[user]['level']=="admin":
				self.owner=user
				break
		self.channels_extended = Dicts("channels", root)
		self.logs = Logs()
		self.commands = {}
		for folder in self.folders+[self.host.split(".")[1]]:
			print "loading from %s folder" % folder
			self.load = Load(folder)
			if not self.commands:
				self.commands.update(self.load.commands)
			else:
				for priority in self.commands:
					self.commands[priority].update(self.load.commands[priority])
		self.replies = replies
		self.init_time = time.time()
		self.stats = File("stats.log")
		self.mnemo = Dicts("memory")

	#Next _on_* are built-in methods to ensure certain tasks
	def _on_ping(self, evt):
		self.pong(evt.msg)
		self.current_sname = evt.msg

	def _on_nick(self, evt):
		if evt.src == self.current_nname:
			self.current_nname = evt.msg
		before = evt.source
		after = evt.msg
		for ch in self.channels.values():
			if ch.has_user(before):
				ch.change_nick(before, after)
		if self.ignored.has_key(before):
			self.ignored[after] = self.ignored.pop(before)
		if self.nicks.has_key(before):
			self.nicks[after] = self.nicks.pop(before)

	def _on_welcome(self, evt):
		self.current_nname = evt.params[0]
		if self.user_passw:
			self.privmsg("NickServ", 'ID %s' % self.user_passw)
			print "waiting for a cloak...",
			for y in range(10,-1,-1):
				print "%i".rjust(2) % y,
				wait(1)
				print "\b"*3,
			print
		self.join(self.defchan)
		wait(.5)
		chans = self.channels_extended.keys()
		for chan in self.channels_extended.keys():
			if not self.channels_extended[chan].get("join"):
				chans.remove(chan)
			if chan == self.defchan:
				chans.remove(chan)
		if len(chans)> 1: self.join(",".join(chans))

	def _on_nicknameinuse(self, evt):
		self.nick("%s_" % self.nickname)
		if self.user_passw:
			wait(.2)
			self.privmsg("NickServ", 'GHOST %s %s' % (self.nickname, self.user_passw))
			wait(.2)
			self.nick(self.nickname)
			wait(.2)
			self.privmsg("NickServ", 'IDENTIFY %s' % self.user_passw)

	def _on_featurelist(self, evt):
		line = " ".join(evt.params)
		features=re.finditer("(?P<key>[A-Z]+)(?:=(?P<value>[^ ]+))?", line)
		for items in features:
			key, value = items.groups()
			parameters = value.split(",") if value and "," in value else value if value else True
			self._features[key.lower()]=parameters
		self.features = Features(self._features)

	def _on_whoisaccount(self, evt):
		self.nicks[evt.params[1]] = evt.params[2]

	def _on_whoisuser(self, evt):
		self.nicks[evt.params[1]] = "%s!%s@%s %s" %(evt.params[1], evt.params[2], evt.params[3], evt.msg)

	def _on_error(self, evt):
		self.quit(evt.msg)

	def _on_nosuchnick(self, evt):
		if self.nicks.has_key(evt.source): self.nicks.pop(evt.source)
		self.ctrl_msg(evt, evt.msg)

	def _on_join(self, evt):
		"""[Internal]"""
		self._get_whois(evt)
		ch = evt.msg if self.features.network() == "Wikimedia" else evt.target.strip()
		nick = evt.source
		if nick == self.current_nname:
			self.channels[ch] = Channel()
		self.channels[ch].add_user(nick)
		stats = self.stats.get()
		if not hasattr(self.channels[ch], "stats"):
			self.channels[ch].stats = [1, time.time()]
		if stats.has_key(ch):
			self.channels[ch].stats[0] +=1
			if stats[ch][0] < self.channels[ch].stats[0]:
				stats[ch] = [self.channels[ch].stats[0], time.time()]
				self.stats.put(stats, True)
		if self._debug[2]:
			print "%s _on_join()\n\tc: %s\n\ts: %r\n\tt: %r\n\tu: %s\n\tn: %s" % (
				__name__, ch, nick, ch,
				self.channels[ch].users(), self.features.network()
			)

		if nick == self.current_nname:
			if self.channels_extended.has_key(ch):
				self.channels[ch].lang = self.channels_extended[ch]['lang']
				self.channels[ch].fam = self.channels_extended[ch]['fam']
				self.channels[ch].listen = self.channels_extended[ch]['listen']
				self.channels[ch].log = self.channels_extended[ch]['log']
				self.channels[ch].reply = self.channels_extended[ch]['reply']
				self.channels[ch].report = self.channels_extended[ch]['report']
				self.channels[ch].triggers = self.channels_extended[ch]['triggers']
				self.channels[ch].colors = self.channels_extended[ch]['colors']
				self.channels[ch].links = self.channels_extended[ch]['links']
				self.channels[ch].join = self.channels_extended[ch]['join']

			if not hasattr(self.channels[ch], 'lang'):
				self.channels[ch].lang = "en"
			if not hasattr(self.channels[ch], 'fam'):
				self.channels[ch].fam = "wikimedia"
			if not hasattr(self.channels[ch], 'listen'):
				self.channels[ch].listen = True
			if not hasattr(self.channels[ch], 'log'):
				self.channels[ch].log = False
			if not hasattr(self.channels[ch], 'reply'):
				self.channels[ch].reply = True
			if not hasattr(self.channels[ch], 'report'):
				self.channels[ch].report = False
			if not hasattr(self.channels[ch], 'triggers'):
				self.channels[ch].triggers = self.triggers
			if not hasattr(self.channels[ch], 'colors'):
				self.channels[ch].colors = False
			if not hasattr(self.channels[ch], 'links'):
				self.channels[ch].links = True
			if not hasattr(self.channels[ch], 'join'):
				self.channels[ch].join = True

	def _on_kick(self, evt):
		"""[Internal]"""
		nick = evt.msg
		channel = evt.params[0]

		if nick == self.current_nname:
			del self.channels[channel]
		else:
			self.channels[channel].remove_user(nick)
			self.channels[channel].stats[0] -=1

	def _on_mode(self, evt):
		"""[Internal]"""
		modes = parse_channel_modes(" ".join(evt.params[1:]))
		t = evt.params[0]
		if is_channel(t):
			ch = self.channels[t]
			for mode in modes:
				if mode[0] == "+":
					f = ch.set_mode
				else:
					f = ch.clear_mode
				f(mode[1], mode[2])
		else:
			# Mode on self... XXX
			pass

	def _on_namreply(self, evt):
		"""[Internal]"""
		# evt.params[1] := "@" for secret channels,
		#                  "*" for private channels,
		#                  "=" for others (public channels)
		# evt.params[2] := channel
		# evt.msg       := nick list

		ch = evt.params[2]
		nicks = evt.msg.split()
		for nick in nicks:
			if nick[0] == "@":
				nick = nick[1:]
				self.channels[ch].set_mode("o", nick)
			elif nick[0] == "+":
				nick = nick[1:]
				self.channels[ch].set_mode("v", nick)
			else:
				self.channels[ch].add_user(nick)
		thread.start_new_thread(self._check_users,(evt,))

		self.channels[ch].stats = [len(nicks), time.time()]
		stats = self.stats.get()
		if stats.has_key(ch):
			if len(nicks) > stats[ch][0]:
				stats[ch] = [len(nicks), time.time()]
				self.stats.put(stats, True)

		if self._debug[2]: print __name__, "_on_namreply()", self.channels[ch].users()

	def _check_users(self, evt):
		#Get whois for each user only for the main bot.
		if self.label[1] > 0: return
		time.sleep(5)
		for chan, _ch in self.channels.items():
			for user in _ch.users():
				if user in ("ChanServ", self.current_nname): continue #+config.otherbots: continue
				if not self.nicks.has_key(user):
				   self.whois(user)
				   time.sleep(.5)
		time.sleep(2)

	def _on_part(self, evt):
		"""[Internal]"""
		nick = evt.source
		channel = evt.params[0].strip()

		if nick == self.current_nname:
			del self.channels[channel]
		else:
			self.channels[channel].remove_user(nick)
			self.channels[channel].stats[0] -=1
		print "WHOIS:",self.nicks


	def _on_quit(self, evt):
		"""[Internal]"""
		nick = evt.source
		for ch in self.channels:
			if self.channels[ch].has_user(nick):
				self.channels[ch].remove_user(nick)
				self.channels[ch].stats[0] -=1

	def _get_whois(self, evt):
		if self.label[1]==0 and evt.agent == "user" and evt.source != self.nickname:
			if evt.source not in self.ignored and not self.nicks.has_key(evt.source):
				self.whois(evt.source)
				self.nicks[evt.source]=None

	#message sending methods
	def send_error(self, evt):
		try:
			import traceback
			trace = traceback.format_exc()
			print trace
			lines = list(reversed(trace.splitlines()))

			report = [""]
			for line in lines:
				line = line.strip()
				f = re.search(r'File ".*[/\\]([^\.]+\.py.*)$', line)
				if f:
					report.append('"'+f.group(1))
					break
				else: report.append(line)
			else: report.append(self.translate('unknown source'))
			report.reverse()
			report="&B;%s&N;; &r;%s&N;; &b;%s&N;" % (report[0],report[1],report[2])
			self.ctrl_msg(evt, report)
			report=re.sub(r"\\x0[23]\d{0,2}", "",  ("%r" % report)[1:-1])
			try:
				self.choose_reply(evt, self.replies.failure, 91)
			except:
				pass
		except: self.ctrl_msg(evt, "Got an error.")

	def ctrl_msg(self, evt, msg):
		"""default/main and control channel"""
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if self.features.network() == "freenode":
			self.privmsg(self.defchan, msg)
		else:
			#must be send to the other bot on the other server.
			mBot = self.bots(("master", 0))
			if evt.type == "privmsg":
				msg = u'<wmrc from="%s"/> %s' % (evt.source, msg)
			mBot.privmsg(mBot.defchan, msg)

	#This method is used to send a message from a "slave" bot to the "master" bot.
	def main_msg(self, evt, target, msg, *args, **keywords):
		"""main channel"""
		mBot = self.bots(("master", 0))
		self._skip_shieldout = True
		if target not in mBot.channels.keys():
			target = mBot.defchan
		if evt.type == "privmsg":
			msg = u'<wmrc from="%s"/> %s' % (evt.source, msg)
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		msg = fmt(msg, *args, **keywords)
		if not mBot.channels[target].reply:
			printf ( u"switch $1 → $2", target, mBot.testchan )
			mBot.privmsg(mBot.testchan, msg)
		else:
			mBot.privmsg(target, msg)

	#next methods are used to comunicate recentchanges on diferent channels
	def vnd_msg(self, evt, msg):
		self.main_msg(evt, "#wikipedia-ca-vandalisme", msg)

	def patrol_msg(self, evt, msg):
		self.main_msg(evt, "#wikipedia-ca-patrol", msg)

	def newpages_msg(self, evt, msg):
		self.main_msg(evt, self.newartchan, msg)

	#next methods are shortening that allows put only the message
	def pub_action(self, evt, msg, *args, **keywords):
		"""Send an action message to the current channel"""
		#if not "$nick" in msg and evt.stalked: msg = "%s: %s" % (evt.source, msg)
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if "%" in msg:
			if args:
				self.action(evt.target, msg % args)
			else:
				self.action(evt.target, msg)
		else:
			if args:
				self.action(evt.target, fmt(msg, args))
			elif args and keywords:
				self.action(evt.target, fmt(msg, *args, **keywords))
			elif not args and keywords:
				self.action(evt.target, fmt(msg, **keywords))
			else:
				self.action(evt.target, msg)


	def pub_msg(self, evt, msg, *args, **keywords):
		"""Send a message to the current channel"""
		#if msg and not "$nick" in msg and evt.stalked: msg=  "%s: %s" % (evt.source, msg)
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if args and not keywords:
			if "%" in msg:
				print "EVT", type(evt.target), evt.target
				args = tuple(encode(arg) for arg in args)
				print "MSG", type(msg), msg % args
				print "ARGS", args
				self.privmsg(encode(evt.target), msg % args)
			else:
				self.privmsg(evt.target, fmt(msg, *args))
		elif args and keywords:
			if "%" in msg:
				self.privmsg(evt.target, msg % keywords)
			else:
				self.privmsg(evt.target, fmt(msg, *args, **keywords))
		elif not args and keywords:
			if "%" in msg:
				self.privmsg(evt.target, msg % keywords)
			else:
				self.privmsg(evt.target, fmt(msg, **keywords))
		else:
			self.privmsg(evt.target, msg)

	def pub_notice(self, evt, msg, bfr=None, *args, **keywords):
		"""Send a notice to the current channel"""
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if args:
			self.notice(evt.target, msg % args)
		else:
			self.notice(evt.target, msg)

	def priv_action(self, evt, msg, *args, **keywords):
		"""Send a private action message to the current nick"""
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if args:
			self.action(evt.source, msg % args)
		else:
			self.action(evt.source, msg)

	def priv_msg(self, evt, msg, *args, **keywords):
		"""Send a private message to the current nick"""
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if args:
			self.privmsg(evt.source, msg % (args))
		else:
			self.privmsg(evt.source, msg)

	def priv_notice(self, evt, msg, *args, **keywords):
		"""Send a private notice to the current nick"""
		printf("PRIV_NOTICE source: $1", evt.source)

		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		if args:
			printf("args($a)", a=args)
			self.notice(evt.source, msg % args)
		else:
			self.notice(evt.source, msg)

	#reply method returns the message with the same way that was received
	#that is: same action/notice/message, same private/public and same channel/nickname
	#if any trouble occurs, it returns a privmsg on default channel.
	def reply(self, evt, msg,  *args, **keywords):
		"""Send a message to someone/some channel using the same event type"""
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		m = re.search("(pub|priv)(msg|notice|action)", evt.type)

		if m:
			reply_obj="_".join(m.groups())
			getattr(self, reply_obj)(evt, msg, *args, **keywords)
		else:
			if args:
				if "%" in msg:
					self.privmsg(self.defchan, msg % args)
				else:
					self.privmsg(self.defchan, fmt(msg, *args))
				#self.privmsg(self.defchan, msg % args)
			elif keywords:
				if "%" in msg:
					self.privmsg(self.defchan, msg % keywords)
				else:
					self.privmsg(self.defchan, fmt(msg, *args, **keywords))
			else:
				self.privmsg(self.defchan, msg)

	#next methods are thougt to return a message with certain delay.
	def write(self, evt, msg, *args):
		"""Send a message to someone/some channel using the same event type simulating a typewriting delay"""
		m = re.search("(pub|priv)(msg|notice|action)", evt.type)
		if args:
			msg = msg % args
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		typetime = float(len(msg))/5
		time.sleep(typetime)
		#msg="%s [%0.3f]" % (msg,typetime)
		if m:
			reply_obj="_".join(m.groups())
			getattr(self, reply_obj)(msg)
		else:
			self.privmsg(self.defchan, msg)

	def write_to(self, evt, type, receptor, msg):
		"""Send a message/action/notice to someone/some channel"""
		typetime = float(len(msg))/5
		#time.sleep(typetime)
		#msg="%s [%0.3f]" % (msg,typetime)
		msg = self._translate(evt, msg)
		msg = self._replace_templates(evt, msg)
		type(receptor, msg)

	#this method simulates random answers (by topics).
	def choose_reply(self, evt, items, prob=100, *args):
		dice = random.randint(0, 100)
		if dice > prob:
			return
		dice = random.randint(0, len(items)-1)
		for tokens in items[dice]:
			if isinstance(tokens[2], basestring):
				if tokens[2] == "t":
					time.sleep(float(len(tokens[1]))/5)
				elif tokens[2].startswith("r"):
					time.sleep(random.randint(1, int(tokens[2][1:])))
			else:
				time.sleep(tokens[2])
			if "%s" in tokens[1]:
				args = args[:tokens[1].count("%s")]
				if tokens[0]=='M': #Message
					self.pub_msg(evt, tokens[1], args)
				elif tokens[0]=='A': #Action (/me)
					self.pub_action(evt, tokens[1], args)
				elif tokens[0]=='N': #Notice
					self.pub_notice(evt, tokens[1], args)
				elif tokens[0]=='m': #Message
					self.priv_msg(evt, tokens[1], args)
				elif tokens[0]=='a': #Action (/me)
					self.priv_action(evt, tokens[1], args)
				elif tokens[0]=='n': #Notice
					self.priv_notice(evt, tokens[1], args)
			elif args and "$" in tokens[1]:
				if tokens[0]=='M':
					self.pub_msg(evt, tokens[1], args)
				elif tokens[0]=='A':
					self.pub_action(evt, tokens[1], args)
				elif tokens[0]=='N':
					self.pub_notice(evt, tokens[1], args)
				elif tokens[0]=='m':
					self.priv_msg(evt, tokens[1], args)
				elif tokens[0]=='a':
					self.priv_action(evt, tokens[1], args)
				elif tokens[0]=='n':
					self.priv_notice(evt, tokens[1], args)
			else:
				if tokens[0]=='M':
					self.pub_msg(evt, tokens[1])
				elif tokens[0]=='A':
					self.pub_action(evt, tokens[1])
				elif tokens[0]=='N':
					self.pub_notice(evt, tokens[1])
				elif tokens[0]=='m':
					self.priv_msg(evt, tokens[1])
				elif tokens[0]=='a':
					self.priv_action(evt, tokens[1])
				elif tokens[0]=='n':
					self.priv_notice(evt, tokens[1])
			time.sleep(tokens[3])

	def _translate(self, evt, string):
		trg = evt.target
		lang = self.channels[trg].lang if is_channel(trg) and self.channels.has_key(trg) else self.accounts.get(self.nicks.get(trg), {"lang":"en"})['lang']
		if hasattr(self, "features") and self.features.network() == "Wikimedia":
			#todo: translate into targeted channel language when wmBot send a msg to frBot
			lang = evt.lang
		return _trans(lang, string)

	def log(self, evt, msg):
		log = File(
			"%(date)s.log" % {
				"date": time.strftime("%y-%m-%d")
			},
			_path.join(_path.home, "logs", self.features.network(), evt.target)
		)
		log.put(msg)

	def reboot(self, msg="REBOOTING..."):
		if not self.bots:
			self.quit()
			return
		for bot in reversed(self.bots.args):
			if bot.is_connected:
				bot.quit(msg)
	#main
	def start(self):
		self._connect()

class Servers:
	def __init__(self, *args):
		self.args=args

	def __call__(self, bot):
		for a_bot in self.args:
			if a_bot.label == bot:
				return a_bot
		raise BadBotDescriptor

	def run(self):
		i=0
		for bot in self.args:
			role = "master" if i == 0 else "slave"
			bot.label = (role, i)
			if i == 0:
				print "*** STREAMING ***", bot.label
				bot._streaming()
			if not hasattr(bot, "is_connected"):
				thread.start_new_thread(bot.start, ())
				bot.is_connected=True
			i+=1
